import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.Properties;

/**
 * This class handles the communication between the client (this machine) and the server.
 * All the connection parameters are loaded from the file "client.conf". The file path is
 * specified in this class, in the configFile attribute.<p>
 *
 * This class extends the Thread class and is launched by the AgentController.<p>
 *
 * The CommunicationThread handles two types of messages:<br/>
 * &nbsp;&nbsp;&nbsp;- Measures (transmitted by the AgentController) which are send
 *    periodically (see client.conf)<br/>
 * &nbsp;&nbsp;&nbsp;- Orders (transmitted by the remote server) which allow a remote
 *    shutdown/reboot from the network administrator<p>
 * @author Alexandre Dos Santos Coelho
 */
public class CommunicationThread extends Thread {

  private final String configFile = "client.conf"; // Client configuration file
  private String server; // Server address
  private int port; // Server connection port
  private int timeout; // Server connection timeout [ms]
  private int reconnectDelay; // Delay [ms] between to reconnection attempts
  private Socket socket; // Communication socket
  private int sendFrequency; // Delay [ms] between sending measures
  private boolean running = false;
  private AgentController agentController; // Reference on the agent controller
  private PrintStream out; // Outgoing communication channel
  private BufferedReader in; // Incomming communication channel
  private LinkedList<Measure> measures;
  private InputThread inputThread;

  /**
   * Constructor. Loads connection configuration and start the thread.
   */
  public CommunicationThread(AgentController a) {
    agentController = a;
    measures = new LinkedList<Measure>(); // Creating measures list
    loadConfig();
    while(!running) { // Connection attemp. If server crashed, retries periodically
      connect();
      try {
        Thread.sleep(reconnectDelay);
      } catch (InterruptedException ex) {}
    }
  }

  /**
   * Connects client to the specified server.
   */
  private void connect() {
    try {
      socket = new Socket(); // Creating socket
      socket.bind(null);
      socket.connect(new InetSocketAddress(server, port), timeout); // Connecting to server
      out = new PrintStream(socket.getOutputStream());
      in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      inputThread = new InputThread(socket, this);
      running = true;
      inputThread.start();
      if(!this.isAlive())
        start();
      System.out.println(Logger.getTime() + "Connected to server");
    } catch (IOException ex) {
      running = false;
      agentController.setConnected(false);
      System.err.println(Logger.getTime() + "Impossible to connect to server");
    }
  }

  /**
   * @param m Measure to transmit to the server. This measure will be added in queue and
   * transmitted persiodically (determined by the sendFrequency in the client.conf)
   */
  public synchronized void addMeasure(Measure m) {
    measures.add(m);
  }

  /**
   * Removes an measure from the sending queue after receiving the confirmation from the server
   * @param id Unique ID of the measure
   */
  public synchronized void removeMeasure(int id) {
    Measure remove=null;
    for(Measure m : measures) {
      if(m.getId()==id) {
        remove = m;
        break;
      }
    }

    if(remove!=null) {
      measures.remove(remove);
    }
  }

  /**
   * Thread run method. First, the client authenticates on the server with its machine name.
   * Once authenticated, the client can send his measures periodically to the server (see
   * sendFrequency variable in client.conf). Those measures will be erased
   * from the sending queue only after a confirmation has been received.<p>
   * 
   * If the server crashes, all the measures are kept in the sending queue and the client will
   * try to reconnect perdiodically (see variable reconnectDelay in client.conf). Once the
   * server is operationnal again, the client re-authenticates and sends the measures he has in queue.
   */
  public void run() {
    while (true) {
      out.println("hello#" + agentController.getMachineName() + "#" + agentController.getMachineType()); // Authenticating on server
      agentController.setConnected(true);
      while (isRunning()) {
        synchronized (this) {
          if (!measures.isEmpty()) {
            inputThread.setConfirm(0); // Reseting server confirmations for received measures
            for (Measure m : measures) { // Sending measures
              System.out.println(Logger.getTime() + "Sent measure: " + m);
              out.println(m);
            }
          }
        }

        // Waiting before next measure sending
        try {
          Thread.sleep(sendFrequency);
        } catch (InterruptedException ex) {
        }
      }
      try {
        socket.close();
        System.out.println(Logger.getTime() + "Disconnected from server");
      } catch (IOException ex) {
      }

      // As the client runs as a service, it has to reconnect after a server crash
      while (socket.isClosed()) {
        System.out.println(Logger.getTime() + "Trying to reconnect to the server...");
        connect();
        if (socket.isConnected()) {
          setRunning(true);
          agentController.setConnected(true);
          inputThread.setRunning(true);
        }
        // Wait before next reconnection attemp
        try {
          Thread.sleep(reconnectDelay);
        } catch (InterruptedException ex) {
        }
      }
    }
  }

  /**
   * Loads the configuration parameters (server address, port, connection timeout,
   * measure send frequency and reconnection delay)
   */
  private void loadConfig() {
    Properties config = new Properties();

    // Open configuration file
    try {
      config.load(new FileInputStream(configFile));
    } catch (IOException ex) {
      System.err.println(Logger.getTime() + "Impossible to open configuration file!");
    }

    // Get configuration elements
    server = (String) config.get("server");
    port = Integer.valueOf((String) config.get("port"));
    timeout = Integer.valueOf((String) config.get("timeout"));
    sendFrequency = Integer.valueOf((String) config.get("sendFrequency"));
    reconnectDelay = Integer.valueOf((String) config.get("reconnectDelay"));
  }

  /**
   * @return the running
   */
  public boolean isRunning() {
    return running;
  }

  /**
   * @param running the running to set
   */
  public void setRunning(boolean running) {
    this.running = running;
  }

  public void setConnected(boolean b) {
    agentController.setConnected(b);
  }
}
